<?php
/*
    ainitramfs functions
    Copyright (C) Ambroz Bizjak, 2010

    This file is part of ainitramfs.

    ainitramfs is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License version 2
    as published by the Free Software Foundation.

    ainitramfs is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/


/**
 * Print an error, cleanup and exit.
 *
 * @param str error message
 */
function ai_error ($str)
{
    echo "Error: $str\n";
    ai_cleanup();
    exit(1);
}

/**
 * Print a warning.
 *
 * @param str message
 */
function ai_warning ($str)
{
    echo "Warning: $str\n";
}

/**
 * Print a notice.
 *
 * @param str message
 */
function ai_notice ($str)
{
    echo "Notice: $str\n";
}

/**
 * Removes the temp directory $ai_temp if it's set, except if $ai_noclean is zero.
 */
function ai_cleanup ()
{
    global $ai_temp;
    global $ai_noclean;

    if ($ai_temp != "") {
        if ($ai_noclean) {
            echo "Not removing temp folder $ai_temp\n";
        } else {
            system("rm -rf ".escapeshellarg($ai_temp), $return);
            if ($return != 0) {
                echo "Cannot remove temp folder $ai_temp\n";
            }
        }
    }
}

/**
 * Splits a path into components.
 * Makes no distinction between absolute and relative paths.
 *
 * @param path path to split
 * @return array of path components
 */
function ai_get_path_components ($path)
{
    $res_comps = array();
    foreach (explode("/", $path) as $comp) {
        if ($comp == "") {
            continue;
        }
        array_push($res_comps, $comp);
    }
    return $res_comps;
}

/**
 * Resolves "." and ".." in an array of path components.
 *
 * @param comp path components
 * @return resulting path components
 */
function ai_resolve_path_components ($comps)
{
    $res_comps = array();
    foreach ($comps as $comp) {
        if ($comp == ".") {
            continue;
        }
        if ($comp == "..") {
            array_pop($res_comps);
            continue;
        }
        array_push($res_comps, $comp);
    }
    return $res_comps;
}

/**
 * Normalizes a path.
 * Replaces multiple slashes with a single one, removes trailing slashes,
 * and resolves ".." and ".".
 */
function ai_path ($path)
{
    if ($path[0] == "/") {
        $res = "/";
    } else {
        $res = "";
    }
    $res .= implode("/", ai_resolve_path_components(ai_get_path_components($path)));
    return $res;
}

/**
 * Joins paths and normalizes the result.
 * If the first path is absolute, the resulting path will be absolute,
 * else it will be relative.
 */
function ai_join_paths ()
{
    $args = func_get_args();
    if (count($args) > 0 && strlen($args[0]) > 0 && $args[0][0] == "/") {
        $res = "/";
    } else {
        $res = "";
    }
    $res .= implode("/", ai_resolve_path_components(call_user_func_array("array_merge", array_map("ai_get_path_components", $args))));
    return $res;
}

/**
 * Returns the directory path of the given path.
 * The path is not normalized.
 *
 * @param path path
 * @return directory path
 */
function ai_dirname ($path)
{
    if ($path[0] == "/") {
        $res = "/";
    } else {
        $res = "";
    }
    $comps = ai_get_path_components($path);
    array_pop($comps);
    $res .= implode("/", $comps);
    return $res;
}

/**
 * Installs a regular file or symlink on the root to the initramfs.
 *
 * @param path absolute path of the file, relative to the root.
 * @param target if the file was a link, returns the pointed-to file
 *        as an absolute path, relative to the root.
 * @param root source root directory, defaults to $ai_root.
 */
function ai_install_fs_file ($path, &$target = NULL, $root = "")
{
    global $ai_root;
    global $ai_out;

    if ($root == "") {
        $root = $ai_root;
    }

    assert($path[0] == '/');
    $path = ai_path($path);
    $fullpath = ai_join_paths($root, $path);
    $fulldest = ai_join_paths($ai_out, $path);

    if (is_link($fullpath)) {
        $link = readlink($fullpath);
        if ($link[0] == '/') {
            $target1 = $link;
        } else {
            $target1 = ai_join_paths(ai_dirname($path), $link);
        }
        ai_install_fs_dir(ai_dirname($path), $root);
        ai_install_fs_file($target1, $target, $root);
        if (!is_link($fulldest)) {
            if (!symlink($link, $fulldest)) {
                ai_error("Cannot create symlink: $fulldest -> $link");
            }
        }
    }
    else if (is_file($fullpath)) {
        $target = $path;
        if (!is_file($fulldest)) {
            ai_install_fs_dir(ai_dirname($path), $root);
            if (!copy($fullpath, $fulldest)) {
                ai_error("Cannot copy file: $fullpath -> $fulldest");
            }
            if (($perms = fileperms($fullpath)) === FALSE) {
                ai_error("Cannot get permissions: $fullpath");
            }
            if (!chmod($fulldest, $perms)) {
                ai_error("Cannot set permissions: $fulldest");
            }
        }
    }
    else {
        ai_error("Cannot install $fullpath, it is neither a regular file nor a symlink");
    }
}

/**
 * Same as {@link ai_install_fs_file}, except that additional dependencies are
 * installed. These dependencies are:
 *   - if the file is a program, shared library dependencies.
 *
 * @param path absolute path of the file, relative to the root.
 */
function ai_install_fs_file_smart ($path)
{
    global $ai_toolchain;
    global $ai_root;
    global $ai_finfo;

    assert($path[0] == '/');

    ai_install_fs_file($path, $target);
    $fulltarget = ai_join_paths($ai_root, $target);

    $info = finfo_file($ai_finfo, $fulltarget);
    if (preg_match('/^ELF/', $info)) {
        ai_install_libs_for_binary($fulltarget);
    }
}

/**
 * Installs a directory on the root to the initramfs.
 *
 * @param path absolute path of the directory, relative to the root.
 * @param root source root directory, defaults to $ai_root.
 */
function ai_install_fs_dir ($path, $root = "")
{
    global $ai_root;
    global $ai_out;

    if ($root == "") {
        $root = $ai_root;
    }

    assert($path[0] == '/');
    $path = ai_path($path);
    $fullpath = ai_join_paths($root, $path);
    $fulldest = ai_join_paths($ai_out, $path);

    if (is_link($fullpath)) {
        if (!is_link($fulldest)) {
            $link = readlink($fullpath);
            if ($link[0] == '/') {
                ai_install_fs_dir($link, $root);
            } else {
                ai_install_fs_dir(ai_join_paths(ai_dirname($path), $link), $root);
            }
            if (!symlink($link, $fulldest)) {
                ai_error("Cannot create symlink: $fulldest -> $link");
            }
        }
    }
    else if (is_dir($fullpath)) {
        if (!is_dir($fulldest)) {
            $dirname = ai_dirname($path);
            if ($dirname != "/") {
                ai_install_fs_dir($dirname, $root);
            }
            if (!mkdir($fulldest)) {
                ai_error("Cannot create directory: $fulldest");
            }
            if (($perms = fileperms($fullpath)) === FALSE) {
                ai_error("Cannot get permissions: $fullpath");
            }
            if (!chmod($fulldest, $perms)) {
                ai_error("Cannot set permissions: $fulldest");
            }
        }
    }
    else {
        ai_error("Cannot install $fullpath, it is neither a directory nor a symlink");
    }
}

function ai_install_fs_smart ($path)
{
    global $ai_root;
    global $ai_out;

    assert($path[0] == '/');
    $path = ai_path($path);
    $fullpath = ai_join_paths($ai_root, $path);

    if (is_dir($fullpath)) {
        ai_install_fs_dir($path);
    } else {
        ai_install_fs_file_smart($path);
    }
}

function ai_install_fs_smart_recursive ($path)
{
    global $ai_root;
    global $ai_out;

    assert($path[0] == '/');
    $path = ai_path($path);
    $fullpath = ai_join_paths($ai_root, $path);

    if (is_dir($fullpath)) {
        ai_install_fs_dir($path);

        foreach (scandir($fullpath) as $entry) {
            if ($entry == "." || $entry == "..") {
                continue;
            }
            $entryfullpath = ai_join_paths($fullpath, $entry);
            $entrypath = ai_join_paths($path, $entry);
            if (is_dir($entryfullpath)) {
                ai_install_fs_smart_recursive($entrypath);
            }
            else if (is_file($entryfullpath)) {
                ai_install_fs_file_smart($entrypath);
            }
        }
    } else {
        ai_install_fs_file_smart($path);
    }
}

/**
 * Installs all libraries a binary depends on to the initramfs.
 *
 * @param bin full path to the binary
 */
function ai_install_libs_for_binary ($bin)
{
    global $ai_toolchain;
    global $ai_root;
    global $ai_temp;

    $bin = ai_path($bin);

    // find libraries

    exec("{$ai_toolchain}readelf -d ".escapeshellarg($bin), $output, $return);
    if ($return != 0) {
        ai_error("Failed to run {$ai_toolchain}readelf -d on {$bin}");
    }

    foreach ($output as $line) {
        if (preg_match('/Shared library: \\[(.+)\\]/', $line, $matches)) {
            $libname = $matches[1];
            $libpath = ai_find_library($libname);
            ai_install_fs_file($libpath, $target);
            ai_install_libs_for_binary(ai_join_paths($ai_root, $target));
        }
    }

    // find interpreter

    unset($output);
    exec("{$ai_toolchain}readelf -l ".escapeshellarg($bin), $output, $return);
    if ($return != 0) {
        ai_error("Failed to run {$ai_toolchain}readelf -l on {$bin}");
    }

    foreach ($output as $line) {
        if (preg_match('/\\[Requesting program interpreter: (.+)\\]/', $line, $matches)) {
            ai_install_fs_file($matches[1]);
        }
    }
}

/**
 * Finds a library by name, looking in library path folders.
 */
function ai_find_library ($libname)
{
    global $ai_ldpaths;
    global $ai_root;

    foreach ($ai_ldpaths as $ldpath) {
        $libpath = ai_join_paths($ldpath, $libname);
        if (file_exists(ai_join_paths($ai_root, $libpath))) {
            return $libpath;
        }
    }

    ai_error("Cannot find library $libname");
}

/**
 * Installs a kernel module and all its dependencies.
 *
 * @param modname name of the module
 */
function ai_install_kmod ($modname)
{
    global $ai_kmodroot;
    global $ai_kmoddir;
    global $ai_kmods;

    if (!array_key_exists($modname, $ai_kmods)) {
        ai_error("Unknown kernel module $modname");
    }

    $modpath = ai_join_paths($ai_kmoddir, $ai_kmods[$modname]);

    ai_install_fs_file($modpath, $target, $ai_kmodroot);

    foreach (ai_read_kmod_depends(ai_join_paths($ai_kmodroot, $target)) as $depmod) {
        ai_install_kmod($depmod);
    }
}

/**
 * Returns a list of direct dependencies of a kernel module.
 *
 * @param file full path to the kernel module
 * @return array containing names of kernel modules the module depends on
 */
function ai_read_kmod_depends ($file)
{
    $depends = exec("modinfo -F depends ".escapeshellarg($file), $output, $return);
    if ($return != 0) {
        ai_error("modinfo failed for $file");
    }

    $res = array();
    foreach (explode(",", $depends) as $depend) {
        if ($depend == "") {
            continue;
        }
        array_push($res, $depend);
    }
    return $res;
}

/**
 * Traverses the kernel modules directory and associates module names with paths.
 *
 * @param prefix directory inside the kernel module directory to look in
 */
function ai_read_kmods ($prefix)
{
    global $ai_kernel;
    global $ai_kmods;
    global $ai_kmodroot;
    global $ai_kmoddir;

    $dir = ai_join_paths($ai_kmodroot, $ai_kmoddir, $prefix);

    foreach (scandir($dir) as $entry) {
        if ($entry == "." || $entry == "..") {
            continue;
        }
        $entry_full = ai_join_paths($dir, $entry);
        if (is_dir($entry_full) && !is_link($entry_full)) {
            ai_read_kmods(ai_join_paths($prefix, $entry));
        }
        else if (is_file($entry_full) && preg_match('/^(.+)\.ko$/', $entry, $matches)) {
            $modname = $matches[1];
            $modprefix = ai_join_paths($prefix, $entry);
            if (array_key_exists($modname, $ai_kmods)) {
                ai_warning("Module {$modname} already known: existing ".$ai_kmods[$modname]." current $modprefix");
            }
            $ai_kmods[$modname] = $modprefix;
        }
    }
}

/**
 * Runs a shell command and returns the return value.
 *
 * @param cmd shell command to run
 * @return return value of the command
 */
function ai_invoke_shell ($cmd)
{
    exec($cmd, $output, $return);
    return $return;
}

/**
 * Reads library paths from ld.so.conf and included files.
 *
 * @param config_file absolute file to file to read
 * @param root directory to which absolute paths in include commands are relative
 * @param paths found library paths will be appended to this array
 * @param concat_result found library paths will be appended to this string as lines
 */
function ai_parse_ldconfig ($config_file, $root, &$paths, &$concat_result)
{
    $lines = file($config_file, FILE_IGNORE_NEW_LINES);

    foreach ($lines as $line) {
        if (preg_match('/^include +([^ ].*)$/', $line, $matches)) {
            $target = $matches[1];
            if ($target[0] == "/") {
                $target = ai_join_paths($root, $target);
            } else {
                $target = ai_join_paths(ai_dirname($config_file), $target);
            }

            foreach(glob($target) as $real_target) {
                ai_parse_ldconfig($real_target, $root, $paths, $concat_result);
            }

            continue;
        }

        if ($line && $line[0] == "/") {
            $paths[] = $line;
            $concat_result .= $line . "\n";
        }
    }
}

/**
 * Enables the given option.
 *
 * @param module option module name
 * @param option option name
 */
function ai_enable_option ($module, $option)
{
    global $ai_options;

    if (!array_key_exists($module, $ai_options) ||
        !array_key_exists($option, $ai_options[$module]) ||
        $ai_options[$module][$option] != "yes"
    ) {
        ai_notice("automatically enabling option: $module $option");
        $ai_options[$module][$option] = "yes";
    }
}
